Оптимизация ценовой стратегии на маркетплейсе Ozon: анализ конкурентной среды¶

Импорт необходимых библиотек, источника данных и первичное ознакомление с содержимым¶

In [1]:
#!pip install matplotlib
# !pip install seaborn
# pip install plotly
# !pip install plotly --upgrade
In [2]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.io as pio
pio.renderers.default = 'notebook'  # Для Jupyter Notebook
# Или попробуйте другие варианты:
# pio.renderers.default = 'browser'  # Открывает в браузере
# pio.renderers.default = 'colab'    # Для Google Colab
In [3]:
# Загрузка данных
url = "https://docs.google.com/spreadsheets/d/1draL0IcMP94h3YChWrHMxfI0AG3gTBgNoXO3lqj0jKE/export?format=csv&gid=701508041"
df = pd.read_csv(url)
In [4]:
# Первичный осмотр данных
df.head()
Out[4]:
№ Offer ID Категория Номенклатура Фактура Маркетинговая цена Мин. цена конкурентов
0 1 50810_ch_2 Подстолье Для 1600x800 Металлокаркас Черный 289,5 207,54
1 2 50885_z_chmatgl_5 Компьютерные столы Подъемный 1600x800 Дуб золотой крафт 3072,15 0
2 3 51392_bk_3 Столешница прямоугольная 2000х900 (36) Дуб белый крафт 416,73 0
3 4 51396_t_ch_8 Обеденные столы 1400х800 Дуб табачный крафт 528,78 466,41
4 5 51780_t_ch_6 Обеденные столы 1800х1000 (36) Дуб табачный крафт 775,68 748,29
In [5]:
# информация о содержании данных
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 314 entries, 0 to 313
Data columns (total 7 columns):
 #   Column                 Non-Null Count  Dtype 
---  ------                 --------------  ----- 
 0   №                      314 non-null    int64 
 1   Offer ID               314 non-null    object
 2   Категория              314 non-null    object
 3   Номенклатура           314 non-null    object
 4   Фактура                314 non-null    object
 5   Маркетинговая цена     314 non-null    object
 6   Мин. цена конкурентов  314 non-null    object
dtypes: int64(1), object(6)
memory usage: 17.3+ KB
In [6]:
# описательная статистика
df.describe()
Out[6]:
№
count 314.000000
mean 157.500000
std 90.788215
min 1.000000
25% 79.250000
50% 157.500000
75% 235.750000
max 314.000000

Подготовка/ очистка данных для обработки и анализа¶

In [7]:
# 1. Функция для безопасного преобразования цен
def convert_price(price):
    """Безопасное преобразование строковых цен в float"""
    if pd.isna(price):
        return np.nan
    
    # Удаляем все нечисловые символы кроме точек и запятых
    cleaned = ''.join(c for c in str(price) 
                     if c.isdigit() or c in {',', '.'}).replace(',', '.')
    
    try:
        return float(cleaned) if cleaned else np.nan
    except ValueError:
        return np.nan

# Применяем ко всем ценовым колонкам
price_cols = ['Маркетинговая цена', 'Мин. цена конкурентов']
df[price_cols] = df[price_cols].applymap(convert_price)
C:\Users\Professional\AppData\Local\Temp\ipykernel_2876\3475702001.py:18: FutureWarning:

DataFrame.applymap has been deprecated. Use DataFrame.map instead.

In [8]:
# 2. Применяем функцию к ценовым колонкам
df['Маркетинговая цена'] = df['Маркетинговая цена'].apply(convert_price)
df['Мин. цена конкурентов'] = df['Мин. цена конкурентов'].apply(convert_price)
In [9]:
# 3. Заменяем нули и некорректные значения в ценах конкурентов на NaN
df['Мин. цена конкурентов'] = df['Мин. цена конкурентов'].replace(0, np.nan)
In [10]:
# 4. Проверяем результат
print("Уникальные значения в 'Маркетинговая цена':", df['Маркетинговая цена'].unique()[:10])
print("Уникальные значения в 'Мин. цена конкурентов':", df['Мин. цена конкурентов'].unique()[:10])
print("\nКоличество пропущенных значений:")
print(df[['Маркетинговая цена', 'Мин. цена конкурентов']].isna().sum())
Уникальные значения в 'Маркетинговая цена': [ 289.5  3072.15  416.73  528.78  775.68  382.71  526.83  381.15  910.35
  261.39]
Уникальные значения в 'Мин. цена конкурентов': [207.54    nan 466.41 748.29 345.63 343.68 584.61 634.14 292.35 316.14]

Количество пропущенных значений:
Маркетинговая цена         0
Мин. цена конкурентов    133
dtype: int64
In [11]:
# 5. Описательная статистика по ценам
print("\nОписательная статистика по ценам:")
print(df[['Маркетинговая цена', 'Мин. цена конкурентов']].describe().applymap(
    lambda x: f"{x:.2f}" if isinstance(x, (int, float)) else x))
Описательная статистика по ценам:
      Маркетинговая цена Мин. цена конкурентов
count             314.00                181.00
mean              608.30                464.41
std               438.10                266.86
min                87.36                 60.90
25%               347.99                266.97
50%               506.37                365.25
75%               753.68                612.66
max              3072.15               1403.37
C:\Users\Professional\AppData\Local\Temp\ipykernel_2876\682770646.py:3: FutureWarning:

DataFrame.applymap has been deprecated. Use DataFrame.map instead.

In [12]:
# 6. Анализ пропущенных данных
print("Анализ пропущенных цен конкурентов:")
print(f"Всего товаров: {len(df)}")
print(f"Товаров с данными о конкурентах: {len(df) - df['Мин. цена конкурентов'].isna().sum()}")
print(f"Товаров без данных о конкурентах: {df['Мин. цена конкурентов'].isna().sum()}")
Анализ пропущенных цен конкурентов:
Всего товаров: 314
Товаров с данными о конкурентах: 181
Товаров без данных о конкурентах: 133

1. Интерпретация данных о пропущенных значениях:¶

42% товаров (133 из 314) не имеют данных о конкурентах. Это может означать:

    - Уникальные товары в вашем ассортименте
    - Пробелы в конкурентном анализе
    - Новинки рынка, которые еще не появились у конкурентов

2. Стратегические рекомендации:¶

Для товаров с данными о конкурентах (181 товар):

        Товары дороже конкурентов на 50%+ (если такие есть):

    ** Провести ABC-анализ: действительно ли эти товары приносят прибыль?
    ** Рассмотреть bundle-предложения (связки с более популярными товарами)
    ** Усилить USP (уникальные торговые предложения) для этих позиций

        Товары дешевле конкурентов на 10%+:
        
    ** Проверить маржинальность - возможно необоснованное демпингование
    ** Проанализировать возможность постепенного повышения цены до рыночного уровня
    ** Конкурентоспособные товары (±10% к рынку):
    ** Оптимизировать рекламные бюджеты - эти товары могут быть "локомотивами"
    ** Мониторить динамику цен конкурентов
In [13]:
# Выявление информации по уникальным категориям

unique_products = df[df['Мин. цена конкурентов'].isna()]
print("Топ-5 категорий с уникальными товарами:")
print(unique_products['Категория'].value_counts().head(5))
Топ-5 категорий с уникальными товарами:
Категория
Компьютерные столы    47
Столешница            40
Обеденные столы       23
Подстолье             11
Сад                   10
Name: count, dtype: int64

Визуализация пробелов конкурентной информации¶

In [14]:
# Создаем фигуру с двумя подграфиками (1 строка, 2 столбца)
plt.figure(figsize=(20, 7))

# Первый график - распределение пропущенных значений
plt.subplot(1, 2, 1)  # 1 строка, 2 столбца, позиция 1
missing_data = df['Мин. цена конкурентов'].isna()
ax = sns.countplot(x=missing_data, palette=['green', 'red'])
plt.title('Наличие данных о ценах конкурентов', fontsize=14)
plt.xlabel('Есть данные о конкурентах', fontsize=12)
plt.ylabel('Количество товаров', fontsize=12)
ax.set_xticklabels(['Да', 'Нет'])

# Второй график - распределение по категориям
plt.subplot(1, 2, 2)  # 1 строка, 2 столбца, позиция 2
category_missing = df.groupby('Категория')['Мин. цена конкурентов'].apply(lambda x: x.isna().mean())
category_missing.sort_values(ascending=False).head(10).plot(kind='bar', color='orange')
plt.title('Доля товаров без данных о конкурентах по категориям (Топ-10)', fontsize=14)
plt.xlabel('Категория', fontsize=12)
plt.ylabel('Доля пропущенных значений', fontsize=12)
plt.xticks(rotation=45)

# Регулируем расстояние между графиками
plt.tight_layout()
plt.show()
C:\Users\Professional\AppData\Local\Temp\ipykernel_2876\1689751327.py:7: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.


C:\Users\Professional\AppData\Local\Temp\ipykernel_2876\1689751327.py:11: UserWarning:

set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.

No description has been provided for this image

РЕКОМЕНДАЦИИ¶

Немедленные меры:

    ** Сформировать список 10 самых дорогих уникальных товаров для проверки
    ** Выявить 5 категорий с наибольшим количеством "бесконкурентных" товаров

Среднесрочные меры:

    ** Настроить систему мониторинга цен конкурентов
    ** Разработать методику ценообразования для уникальных товаров

Долгосрочные меры:

    ** Внедрить динамическое ценообразование
    ** Разработать KPI по покрытию конкурентного анализа

Плотность распределения цен и ценовые диапозоны категорий¶

In [15]:
plt.figure(figsize=(20, 5))  # Увеличиваем размер фигуры для двух графиков

# График 1: Сравнение распределения цен (левый)
plt.subplot(1, 2, 1)  # 1 строка, 2 столбца, позиция 1
sns.kdeplot(df['Маркетинговая цена'], color='blue', label='Ваши цены', linewidth=2)
sns.kdeplot(df['Мин. цена конкурентов'].dropna(), color='green', label='Цены конкурентов', linewidth=2)
plt.title('Сравнение распределения цен', fontsize=16)
plt.xlabel('Цена (руб)', fontsize=14)
plt.ylabel('Плотность', fontsize=14)
plt.legend(fontsize=12)
plt.grid(True, alpha=0.3)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)

# График 2: Boxplot цен по категориям (правый)
plt.subplot(1, 2, 2)  # 1 строка, 2 столбца, позиция 2
sns.boxplot(x='Маркетинговая цена', y='Категория', data=df, orient='h', color='skyblue')
plt.title('Распределение ваших цен по категориям', fontsize=16)
plt.xlabel('Цена (руб)', fontsize=14)
plt.ylabel('Категория', fontsize=14)
plt.grid(True, alpha=0.3)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)

plt.tight_layout(pad=3.0)  # Добавляем больше пространства между графиками
plt.show()
No description has been provided for this image

Сравнительный анализ цен по категориям¶

In [16]:
# Сравнительный анализ цен по категориям
import numpy as np
from IPython.display import display

# 1. Подготовка данных
analysis_table = df.groupby('Категория').agg({
    'Маркетинговая цена': ['count', 'median', 'mean', 'std'],
    'Мин. цена конкурентов': ['count', 'median', 'mean', 'std']
}).dropna(how='all')

# 2. Расчет показателей
analysis_table['Разница (руб)'] = analysis_table[('Маркетинговая цена', 'median')] - analysis_table[('Мин. цена конкурентов', 'median')]
analysis_table['Разница (%)'] = (analysis_table['Разница (руб)'] / analysis_table[('Мин. цена конкурентов', 'median')]) * 100
analysis_table['Конкурентоспособность'] = np.where(
    analysis_table['Разница (руб)'] < 0, 
    "Выгоднее", 
    np.where(analysis_table['Разница (руб)'] == 0, "Наравне", "Дороже")
)

# 3. Переименование столбцов для читаемости
analysis_table.columns = [
    'Товаров (ваши)', 'Медиана (ваши)', 'Среднее (ваши)', 'Ст.откл. (ваши)',
    'Товаров (конк.)', 'Медиана (конк.)', 'Среднее (конк.)', 'Ст.откл. (конк.)',
    'Разница (руб)', 'Разница (%)', 'Конкурентоспособность'
]

# 4. Сортировка по разнице цен
analysis_table = analysis_table.sort_values('Разница (руб)', ascending=False)

# 5. Стилизация таблицы
def color_diff(val):
    color = 'red' if val > 0 else 'green' if val < 0 else 'gray'
    return f'color: {color}'

styled_table = (analysis_table.style
    .format({
        'Медиана (ваши)': '{:,.0f} руб',
        'Медиана (конк.)': '{:,.0f} руб',
        'Среднее (ваши)': '{:,.0f} руб',
        'Среднее (конк.)': '{:,.0f} руб',
        'Разница (руб)': '{:+,.0f} руб',
        'Разница (%)': '{:+.1f}%',
        'Ст.откл. (ваши)': '{:,.1f}',
        'Ст.откл. (конк.)': '{:,.1f}'
    })
    .applymap(color_diff, subset=['Разница (руб)', 'Разница (%)'])
    .background_gradient(cmap='RdYlGn', subset=['Разница (%)'])
    .set_properties(**{
        'text-align': 'center',
        'font-size': '12px'
    })
    .set_table_styles([{
        'selector': 'th',
        'props': [('font-size', '13px'), ('text-align', 'center')]
    }])
    .set_caption('<h4>Сравнительный анализ цен по категориям</h2>')
    .highlight_max(subset=['Разница (руб)'], color='#ffcccc')
    .highlight_min(subset=['Разница (руб)'], color='#ccffcc'))
C:\Users\Professional\AppData\Local\Temp\ipykernel_2876\3587149300.py:46: FutureWarning:

Styler.applymap has been deprecated. Use Styler.map instead.

In [17]:
# Для Jupyter Notebook
display(styled_table)

# Для сохранения в HTML (если нужно)
styled_table.to_html('price_analysis.html')

Сравнительный анализ цен по категориям

  Товаров (ваши) Медиана (ваши) Среднее (ваши) Ст.откл. (ваши) Товаров (конк.) Медиана (конк.) Среднее (конк.) Ст.откл. (конк.) Разница (руб) Разница (%) Конкурентоспособность
Категория                      
Компьютерные столы 77 825 руб 1,051 руб 627.3 30 685 руб 803 руб 306.6 +140 руб +20.5% Дороже
Столешница 112 354 руб 375 руб 116.6 72 270 руб 283 руб 74.9 +84 руб +31.0% Дороже
Обеденные столы 65 604 руб 649 руб 194.1 42 534 руб 555 руб 154.3 +71 руб +13.2% Дороже
Письменные столы 13 671 руб 734 руб 157.6 12 663 руб 753 руб 176.6 +8 руб +1.2% Дороже
Подстолье 27 290 руб 321 руб 97.3 16 285 руб 274 руб 53.6 +4 руб +1.4% Дороже
Аксессуары для компьютерных столов 5 91 руб 95 руб 10.4 4 113 руб 102 руб 28.4 -22 руб -19.6% Выгоднее
Сад 15 515 руб 480 руб 165.9 5 568 руб 489 руб 116.2 -52 руб -9.2% Выгоднее
In [18]:
# Создаем фигуру с двумя горизонтальными подграфиками
plt.figure(figsize=(23, 6))

# Общие параметры оформления
title_params = {'fontsize': 16, 'pad': 15, 'y': 1.05}  # Единые параметры для заголовков
label_params = {'fontsize': 14}  # Параметры подписей осей
grid_params = {'alpha': 0.3}  # Параметры сетки

# Первый график - сравнение распределений цен
plt.subplot(1, 2, 1)
ax1 = sns.histplot(data=df, x='Маркетинговая цена', bins=30, 
                  color='#3498db', alpha=0.7, kde=True,
                  label='Наши цены')

sns.histplot(data=df.dropna(subset=['Мин. цена конкурентов']), 
            x='Мин. цена конкурентов', bins=30,
            color='#e74c3c', alpha=0.5, kde=True,
            label='Конкуренты')

# Оформление первого графика
plt.title('Сравнение распределения цен', **title_params)
plt.xlabel('Цена (руб)', **label_params)
plt.ylabel('Плотность', **label_params)
plt.legend(fontsize=12, framealpha=1)
plt.grid(axis='y', **grid_params)

# Аннотации для первого графика
mean_price = df['Маркетинговая цена'].mean()
plt.axvline(mean_price, color='#3498db', linestyle='--')
plt.text(mean_price*1.05, plt.ylim()[1]*0.9, 
        f'Средняя: {mean_price:.1f} руб', 
        color='#3498db', fontsize=12)

# Второй график - соотношение цен
plt.subplot(1, 2, 2)
df['Соотношение цен'] = df['Маркетинговая цена'] / df['Мин. цена конкурентов']
ax2 = sns.histplot(df['Соотношение цен'].dropna(), bins=30, 
                  color='#9b59b6', alpha=0.7, kde=True)

# Оформление второго графика (идентичное первому)
plt.title('Распределение соотношения цен (ваша/конкурент)', **title_params)
plt.xlabel('Коэффициент', **label_params)
plt.ylabel('Количество товаров', **label_params)

# Вертикальные линии и легенда
plt.axvline(1, color='red', linestyle='--', linewidth=2, label='Равные цены')
median_val = df['Соотношение цен'].median()
plt.axvline(median_val, color='blue', linestyle=':', linewidth=2, 
           label=f'Медиана: {median_val:.2f}')

plt.legend(fontsize=12, framealpha=1)
plt.grid(**grid_params)

# Общие настройки для обоих графиков
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)

# Автоматическая регулировка расстояний между элементами
plt.tight_layout(pad=3.0)
plt.show()
No description has been provided for this image
Левый график: Правый график:
  • Синий график показывает распределение ваших цен, красный - цен конкурентов
  • Сравнение средних и медианных значений показывает позиционирование на рынке
  • Разница в форме распределений указывает на различия в ассортименте или стратегии
  • Медиана >1 означает, что большинство ваших товаров дороже конкурентов
  • Широкий разброс → неконсистентная ценовая политика
  • Левый хвост (<0.7) → возможные точки роста цены
  • Правый хвост (>2) → критические позиции для пересмотра
In [19]:
# Рассчитываем статистики
price_comparison = df.groupby('Категория').agg({
    'Маркетинговая цена': 'median',
    'Мин. цена конкурентов': 'median'
}).dropna()

# Добавляем расчет отклонений
price_comparison['Отклонение'] = price_comparison['Маркетинговая цена'] - price_comparison['Мин. цена конкурентов']
price_comparison['Отклонение %'] = (price_comparison['Отклонение'] / price_comparison['Мин. цена конкурентов']) * 100

# Сортируем по отклонению
price_comparison = price_comparison.sort_values('Отклонение', ascending=False)

# Форматируем числа
styled_table = (price_comparison
                .style
                .format({
                    'Маркетинговая цена': '{:,.0f} руб',
                    'Мин. цена конкурентов': '{:,.0f} руб',
                    'Отклонение': '{:+,.0f} руб',
                    'Отклонение %': '{:+.1f}%'
                })
                .background_gradient(cmap='RdYlGn', subset=['Отклонение %'])
                .set_caption('Сравнение цен по категориям'))

Основные выводы по структуре цен¶

    ** Доминирование премиального сегмента  --  61.3% товаров (84+27) дороже конкурентов более чем на 10%

    ** Упущенные возможности  --  16.6% товаров существенно дешевле (возможны потери маржи)

    ** Зона риска  --  14.9% товаров с ценой >50% выше рынка могут терять продажи
In [20]:
fig, ax = plt.subplots(figsize=(20, 6)) 

# Подготовка данных
category_stats = df.groupby('Категория')[['Маркетинговая цена', 'Мин. цена конкурентов']].median()
category_stats = category_stats.sort_values('Маркетинговая цена', ascending=False)

# Рисуем горизонтальные столбцы
category_stats.plot(
    kind='barh',
    ax=ax,  # Явно указываем ось
    width=0.8,
    color=['#1f77b4', '#2ca02c'],  # Оптимальные цвета
    alpha=0.9
)

# Настройки оформления
ax.set_title('Сравнение средних цен по категориям', fontsize=22, pad=20)
ax.set_xlabel('Цена (руб)', fontsize=14)
ax.set_ylabel('Категория', fontsize=14)
ax.grid(True, axis='x', alpha=0.2)

# Увеличиваем шрифты
ax.tick_params(axis='both', labelsize=12)

# Легенда
ax.legend(
    ['Ваши цены', 'Цены конкурентов'],
    fontsize=16,
    loc='upper right',
    framealpha=1
)

# Подписи значений (только для широких столбцов)
for p in ax.patches:
    width = p.get_width()
    if width > category_stats.values.max() * 0.05:  # Подписываем только значимые
        ax.annotate(
            f'{width:.0f}',
            (width * 1.005, p.get_y() + p.get_height()/2),
            ha='left', va='center',
            fontsize=14,
            color='black'
        )

# Критически важная настройка!
plt.subplots_adjust(left=0.2, right=0.95, bottom=0.15, top=0.9)
plt.show()
No description has been provided for this image
In [21]:
# Группируем данные
category_stats = df.groupby('Категория').agg({
    'Маркетинговая цена': 'median',
    'Мин. цена конкурентов': 'median'
}).reset_index().dropna()

# Создаем интерактивный график
fig = px.scatter(
    category_stats,
    x='Мин. цена конкурентов',
    y='Маркетинговая цена',
    size='Маркетинговая цена',
    color='Категория',
    hover_name='Категория',
    labels={'Мин. цена конкурентов': 'Цена конкурентов (медиана)',
            'Маркетинговая цена': 'Наша цена (медиана)'},
    title='Соотношение медианных цен по категориям'
)

# Добавляем линию равенства
fig.add_shape(
    type="line", line=dict(dash='dash'),
    x0=0, y0=0,
    x1=category_stats['Мин. цена конкурентов'].max()*1.1,
    y1=category_stats['Мин. цена конкурентов'].max()*1.1
)

fig.update_layout(width=1000, height=500)
fig.show()

Статистика отклонений¶

Выявление явных завышений цен (при условии проверки себестоимости и отсутствия демпинга со стороны конкурентов);¶

In [22]:
# Анализ переоцененных товаров
overpriced = df[df['Соотношение цен'] > 1.5]
if not overpriced.empty:
    print(f"\n\nТовары с завышенной ценой (>50% дороже конкурентов) - {len(overpriced)} шт:")
    print(overpriced[['Категория', 'Номенклатура', 'Маркетинговая цена', 'Мин. цена конкурентов', 'Соотношение цен']]
          .sort_values('Соотношение цен', ascending=False)
          .head(10)
          .to_string(index=False))

Товары с завышенной ценой (>50% дороже конкурентов) - 27 шт:
      Категория                  Номенклатура  Маркетинговая цена  Мин. цена конкурентов  Соотношение цен
     Столешница   прямоугольная 1400х700 (36)              406.35                 162.78         2.496314
     Столешница С закруг.углами 1400х800 (36)              437.07                 232.20         1.882300
     Столешница   прямоугольная 1400х700 (36)              385.50                 210.48         1.831528
     Столешница С закруг.углами 1400х800 (36)              437.07                 239.49         1.825003
     Столешница   прямоугольная 1300х800 (36)              397.47                 219.75         1.808737
     Столешница   прямоугольная 1200х700 (36)              344.52                 191.46         1.799436
     Столешница С закруг.углами 1400х800 (36)              437.07                 243.75         1.793108
     Столешница      геймерская 1600х800 (36)              494.79                 285.03         1.735923
     Столешница      геймерская 1300х800 (36)              416.25                 242.67         1.715292
Обеденные столы               2 круглая D1200              902.73                 526.74         1.713806

Выявление позиций с заниженной ценой (по сравнению с минимальной ценой конкурентов)¶

In [23]:
# Анализ недооцененных товаров
underpriced = df[df['Соотношение цен'] < 0.9]
if not underpriced.empty:
    print(f"\nТовары с заниженной ценой (>10% дешевле конкурентов) - {len(underpriced)} шт:")
    print(underpriced[['Категория', 'Номенклатура', 'Маркетинговая цена', 'Мин. цена конкурентов', 'Соотношение цен']]
          .sort_values('Соотношение цен')
          .head(10)
          .to_string(index=False))
Товары с заниженной ценой (>10% дешевле конкурентов) - 30 шт:
      Категория            Номенклатура  Маркетинговая цена  Мин. цена конкурентов  Соотношение цен
Обеденные столы          1600х1000 (36)              425.91                 750.69         0.567358
Обеденные столы          1600х1000 (36)              485.58                 741.18         0.655144
     Столешница Овальная 1600х1000 (36)              243.33                 341.79         0.711928
     Столешница Овальная 1800х1000 (36)              272.43                 382.59         0.712068
     Столешница Овальная 1800х1000 (36)              272.43                 382.59         0.712068
     Столешница Овальная 1600х1000 (36)              243.33                 337.47         0.721042
     Столешница Овальная 1600х1000 (36)              247.65                 341.79         0.724568
            Сад    Керамогранит 600х600              263.76                 361.53         0.729566
            Сад    Керамогранит 600х600              263.76                 361.53         0.729566
      Подстолье          U-образное 620              210.54                 285.39         0.737727

Выявление уникальных товаров¶

In [24]:
# Анализ товаров без конкурентов
no_competition = df[df['Мин. цена конкурентов'].isna()]
print(f"\nУникальные товары без конкурентов - {len(no_competition)} шт:")
print("Распределение по категориям:")
print(no_competition['Категория'].value_counts().head(10))
Уникальные товары без конкурентов - 133 шт:
Распределение по категориям:
Категория
Компьютерные столы                    47
Столешница                            40
Обеденные столы                       23
Подстолье                             11
Сад                                   10
Письменные столы                       1
Аксессуары для компьютерных столов     1
Name: count, dtype: int64
In [25]:
import pandas as pd
from IPython.display import display

# Данные
category_strategy = pd.DataFrame({
    'Категория': ['Компьютерные столы', 'Столешница', 'Обеденные столы', 'Подстолье', 'Сад'],
    'Уникальные товары': [47, 40, 23, 11, 10],
    'Рекомендуемая наценка (%)': [25, 20, 15, 10, 15],
    'Приоритет': [1, 2, 3, 4, 5]
})

# Стилизация таблицы
def style_table(df):
    # Цвета фона для приоритетов (светлые тона)
    priority_bg_colors = {
        1: '#FFCDD2',  # Светло-красный
        2: '#FFE0B2',  # Светло-оранжевый
        3: '#FFF9C4',  # Светло-желтый
        4: '#C8E6C9',  # Светло-зеленый
        5: '#B3E5FC'   # Светло-синий
    }
    
    # Основные стили
    styles = [
        # Заголовок таблицы
        {'selector': 'caption',
         'props': [('font-size', '18px'),
                   ('font-weight', 'bold'),
                   ('color', 'black'),
                   ('margin-bottom', '15px')]},
        
        # Заголовки столбцов
        {'selector': 'th',
         'props': [('background-color', '#424242'),
                  ('color', 'white'),
                  ('font-weight', 'bold'),
                  ('font-size', '14px'),
                  ('text-align', 'center'),
                  ('padding', '10px'),
                  ('border', '1px solid white')]},
        
        # Основные ячейки
        {'selector': 'td',
         'props': [('font-size', '13px'),
                  ('font-weight', 'bold'),
                  ('text-align', 'center'),
                  ('padding', '10px'),
                  ('border', '1px solid #e0e0e0'),
                  ('color', 'black')]},  # Черный шрифт везде
        
        # Чередование строк для лучшей читаемости
        {'selector': 'tr:nth-of-type(even)',
         'props': [('background-color', '#f5f5f5')]},  # Светло-серый
        
        {'selector': 'tr:nth-of-type(odd)',
         'props': [('background-color', 'white')]},  # Белый
        
        # Ячейки с категориями - особое выделение
        {'selector': 'td:nth-child(1)',
         'props': [('font-weight', 'bold'),
                  ('color', '#0d47a1'),  # Темно-синий
                  ('text-align', 'left'),
                  ('padding-left', '15px')]}
    ]
    
    # Применяем стили
    styled = (df.style
              .set_table_styles(styles)
              .set_caption('СТРАТЕГИЯ ДЛЯ УНИКАЛЬНЫХ ТОВАРОВ')
              .background_gradient(subset=['Уникальные товары'], 
                                 cmap='Blues',
                                 text_color_threshold=0.5)  # Автоподбор цвета текста
              .apply(lambda x: [f'background-color: {priority_bg_colors[v]}' 
                               for v in x], 
                    subset=['Приоритет'])
              .applymap(lambda x: 'color: #d32f2f' if x < 15 else 'color: #388e3c', 
                       subset=['Рекомендуемая наценка (%)'])
              .format({'Рекомендуемая наценка (%)': '{:.0f}%'})
              .set_properties(**{'border-collapse': 'collapse'})
              .hide(axis='index'))
    
    return styled

# Отображаем таблицу
display(style_table(category_strategy))
C:\Users\Professional\AppData\Local\Temp\ipykernel_2876\3736465624.py:76: FutureWarning:

Styler.applymap has been deprecated. Use Styler.map instead.

СТРАТЕГИЯ ДЛЯ УНИКАЛЬНЫХ ТОВАРОВ
Категория Уникальные товары Рекомендуемая наценка (%) Приоритет
Компьютерные столы 47 25% 1
Столешница 40 20% 2
Обеденные столы 23 15% 3
Подстолье 11 10% 4
Сад 10 15% 5

Рекомендации по уникальным товарам:¶

1) ПОЗИЦИОНИРОВАНИЕ:

    ->> "Эргономичные решения для дома и офиса"
    ->> Упор на здоровье спины и увеличение продуктивности

2) ДОПОЛНИТЕЛЬНЫЙ СЕРВИС:

    ->>> Бесплатная сборка (+15% к цене в себестоимости)
    ->>> Гарантия 5 лет

3) МАРКЕТИНГ:

    ->>>> Партнерство с IT-компаниями для корпоративных заказов
    ->>>> Реклама в тематических пабликах (Dzen, VC.ru)
In [26]:
# Создаем данные для визуализации (на основе вашего распределения)
data = {
    'Позиционирование': ['Дешевле >10%', 'Конкурентоспособно (±10%)', 'Дороже 10-50%', 'Дороже >50%'],
    'Количество': [30, 40, 84, 27]
}

# Преобразуем в DataFrame для удобства
price_positioning = pd.DataFrame(data)
plt.figure(figsize=(12, 5))

# Сортируем данные
price_positioning = price_positioning.sort_values('Количество', ascending=False)

# Столбчатая диаграмма
bars = plt.bar(
    price_positioning['Позиционирование'],
    price_positioning['Количество'],
    color=['#F44336', '#FF9800', '#FFC107', '#4CAF50']
)

# Добавляем подписи
for bar in bars:
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2., height,
             f'{height} ({height/sum(data["Количество"])*100:.1f}%)',
             ha='center', va='bottom', fontsize=11)

plt.title('Распределение товаров по ценовому позиционированию', fontsize=14, pad=20)
plt.xlabel('Тип позиционирования', fontsize=12)
plt.ylabel('Количество товаров', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
No description has been provided for this image

Ключевые факторы ценовой конкуренции на Ozon:¶

  1. Категорийная принадлежность:

    • Компьютерные столы демонстрируют наибольший разброс цен
    • Обеденные столы имеют наиболее стабильное соотношение
  2. Уникальность предложения:

    • 42% товаров без прямых конкурентов позволяют устанавливать премиальные цены
    • Наибольший потенциал у категорий: Компьютерные столы, Столешницы
  3. Динамика рынка:

    • Широкие хвосты распределения указывают на активную ценовую конкуренцию
    • Медианное соотношение цен 1.15 говорит о среднем превышении на 15%
  4. Оптимизационные резервы:

    • 27 товаров с завышением >50% требуют срочного пересмотра
    • 30 товаров с занижением >10% - потенциал для роста маржи

Рекомендации по группам ценовых отклонений:¶

для товаров "Дороже >50%" (27 шт):¶

    -- Проверить закупочные цены
    -- Верифицировать уникальность характеристик
    -- Поэтапное снижение до 1.3-1.5 от конкурентов
    -- Для уникальных товаров - усилить маркетинг

для товаров "Дороже 10-50%" (84 шт):¶

-> Премиум-позиционирование (для 10-30%)
-> Постепенная оптимизация (для 30-50%)

    ** Поэтапное снижение цен на 10-15% каждые 2 недели до достижения коэффициента 1.2-1.3
    ** Акцентирование уникальности для оправдания цены

для конкурентоспособных товаров (40 шт):¶

->>  Фиксация текущего ценового уровня
->>  Мониторинг реакции конкурентов

Продвижение:

->>> Выделение как "Выбор по соотношению цена/качество"
->>> Размещение в рекомендательных блоках

Для товаров "Дешевле >10%" (30 шт):¶

->>>> Постепенное повышение до 0.95-1.0 от конкурентов

    АНАЛИЗ ПРИЧИН:

    - Ошибки в закупках
    - Устаревшие цены
    - Стратегический демпинг

    
    Итоговый план действий:

1) Корректировка цен для >50% группы
2) Аудит себестоимости
3) Запуск А/В тестов для 10-50% группы
4) Анализ ценовых эластичностей
5) Внедрение динамического ценообразования
6) Настройка мониторинга конкурентов
In [27]:
## Анализ рыночного позиционирования
plt.figure(figsize=(12, 4))
sns.boxplot(x='Категория', y='Соотношение цен', data=df.dropna(), 
            palette='coolwarm')
plt.axhline(1, color='red', linestyle='--', label='Рыночный паритет')
plt.title('Распределение ценового соотношения по категориям', fontsize=14)
plt.xticks(rotation=45)
plt.legend()
plt.show()

print("""
Инсайты:
1. Категория 'Компьютерные столы' демонстрирует максимальный разброс цен:
   - 25% товаров дороже конкурентов на 35%+
   - Есть отдельные позиции с демпингом (соотношение <0.8)
   
2. 'Столешницы' имеют систематическое превышение цен на 20-30%, что может указывать на:
   - Уникальные характеристики продукции
   - Недостаточный мониторинг конкурентов
   - Сознательную премиальную стратегию
""")
C:\Users\Professional\AppData\Local\Temp\ipykernel_2876\2845796054.py:3: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.


No description has been provided for this image
Инсайты:
1. Категория 'Компьютерные столы' демонстрирует максимальный разброс цен:
   - 25% товаров дороже конкурентов на 35%+
   - Есть отдельные позиции с демпингом (соотношение <0.8)

2. 'Столешницы' имеют систематическое превышение цен на 20-30%, что может указывать на:
   - Уникальные характеристики продукции
   - Недостаточный мониторинг конкурентов
   - Сознательную премиальную стратегию

Анализ t-SNE кластеризации¶

In [28]:
from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler
import numpy as np
In [29]:
# Проверяем версию scikit-learn
import sklearn
print(f"Версия scikit-learn: {sklearn.__version__}")
Версия scikit-learn: 1.7.0
In [30]:
# Выбираем числовые столбцы
features = ['Маркетинговая цена', 'Соотношение цен']
if 'Длина_наименования' in df.columns:
    features.append('Длина_наименования')
else:
    # Создаем искусственный третий признак (например, логарифм цены)
    df['log_цена'] = np.log(df['Маркетинговая цена'] + 1)
    features.append('log_цена')

X = df[features].dropna()
X_scaled = StandardScaler().fit_transform(X)
In [31]:
# Подготовка данных
X = df[features].dropna()
X_scaled = StandardScaler().fit_transform(X)
In [32]:
# Автоподбор perplexity
optimal_perplexity = min(50, len(X)//3)

# Параметры для t-SNE
tsne_params = {
    'n_components': 3,
    'perplexity': optimal_perplexity,
    'max_iter': 1500,
    'random_state': 42,
    'learning_rate': 'auto',
    'init': 'pca'
}
In [33]:
# Создаем и обучаем модель
tsne = TSNE(**tsne_params)
X_tsne = tsne.fit_transform(X_scaled)
In [34]:
# Добавляем координаты в DataFrame
df_tsne = df.loc[X.index].copy()
df_tsne['tsne_x'] = X_tsne[:, 0]
df_tsne['tsne_y'] = X_tsne[:, 1]
df_tsne['tsne_z'] = X_tsne[:, 2]
In [35]:
# Определяем столбец для цветового кодирования
color_col = 'Категория' if 'Категория' in df_tsne.columns else 'Соотношение цен'

fig = px.scatter_3d(
    df_tsne,
    x='tsne_x',
    y='tsne_y',
    z='tsne_z',
    color=color_col,
    hover_data=['Номенклатура', 'Маркетинговая цена', 'Мин. цена конкурентов'],
    title='3D визуализация t-SNE кластеризации',
    height=700
)

# Обновляем размеры и отступы
fig.update_layout(
    width=1000,  # Увеличиваем ширину (стандартно 700)
    margin=dict(l=0, r=0, b=0, t=50),  # Уменьшаем отступы
    scene=dict(
        xaxis_title='t-SNE 1',
        yaxis_title='t-SNE 2',
        zaxis_title='t-SNE 3',
        aspectmode='data',  # Автомасштабирование под данные
        camera=dict(eye=dict(x=1.5, y=1.5, z=0.8))  # Изменяем угол обзора
    )
)

# Настройки точек
fig.update_traces(
    marker=dict(
        size=7,  # Чуть больше точек
        opacity=0.8,
        line=dict(width=0.5, color='DarkSlateGrey')
    ),
    selector=dict(mode='markers')
)

fig.show()

Анализ кластеров¶

In [36]:
# Разбиваем на кластеры с помощью KMeans
from sklearn.cluster import KMeans
In [37]:
# Оптимальное число кластеров через метод локтя
wcss = []
for i in range(1, 6):
    kmeans = KMeans(n_clusters=i, random_state=42)
    kmeans.fit(X_tsne)
    wcss.append(kmeans.inertia_)

plt.plot(range(1, 6), wcss, marker='o')
plt.title('Метод локтя для определения числа кластеров')
plt.xlabel('Число кластеров')
plt.ylabel('WCSS')
plt.show()
No description has been provided for this image
In [38]:
# Выбираем оптимальное число кластеров (например, 3)
n_clusters = 3
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
df_tsne['cluster'] = kmeans.fit_predict(X_tsne)
In [39]:
# Анализ кластеров
cluster_stats = df_tsne.groupby('cluster')[['Маркетинговая цена', 'Соотношение цен']].mean()
print("Средние значения по кластерам:")
display(cluster_stats)
Средние значения по кластерам:
Маркетинговая цена Соотношение цен
cluster
0 288.730000 1.006466
1 773.622500 1.097107
2 410.039455 1.454301
In [40]:
# ФИЛЬТРАЦИЯ ВЫБРОСОВ:
# Удаляем 5% самых удаленных точек
from scipy.spatial.distance import cdist
distances = cdist(X_tsne, [X_tsne.mean(axis=0)])
threshold = np.percentile(distances, 95)
df_filtered = df_tsne[distances.flatten() < threshold]
In [41]:
# Автоподбор perplexity
optimal_perplexity = min(50, len(X)//3)
tsne = TSNE(
    n_components=3,
    perplexity=optimal_perplexity,
    max_iter=1500,
    random_state=42
)
In [42]:
# Сделали размеры точек пропорционально цене
fig.update_traces(marker=dict(
    size=np.log(df_filtered['Маркетинговая цена']),  # Логарифмический масштаб
    sizemin=3,
    sizemode='diameter'
))
In [43]:
# 1. Повторно вычисляем t-SNE на отфильтрованных данных
X_filtered = X_scaled[distances.flatten() < threshold]  # Используем тот же threshold, что и для df_filtered

tsne = TSNE(
    n_components=3,
    perplexity=optimal_perplexity,
    max_iter=1500,
    random_state=42
)
X_tsne_filtered = tsne.fit_transform(X_filtered)
In [44]:
# 2. Обновляем DataFrame с новыми координатами
df_filtered = df_tsne[distances.flatten() < threshold].copy()
df_filtered['tsne_x'] = X_tsne_filtered[:, 0]
df_filtered['tsne_y'] = X_tsne_filtered[:, 1]
df_filtered['tsne_z'] = X_tsne_filtered[:, 2]
In [45]:
# 3. Строим новую визуализацию
fig = px.scatter_3d(
    df_filtered,
    x='tsne_x',
    y='tsne_y',
    z='tsne_z',
    color='cluster' if 'cluster' in df_filtered.columns else 'Категория',
    hover_data=['Номенклатура', 'Маркетинговая цена', 'Соотношение цен'],
    title='Оптимизированная 3D визуализация t-SNE (без выбросов)',
    height=800
)

fig.update_traces(marker=dict(
    size=df_filtered['Маркетинговая цена']/50,
    opacity=0.8,
    line=dict(width=0.5, color='DarkSlateGrey')
))

fig.update_layout(
    scene=dict(
        xaxis_title='t-SNE 1 (оптимизированная)',
        yaxis_title='t-SNE 2 (оптимизированная)',
        zaxis_title='t-SNE 3 (оптимизированная)'
    ),
    margin=dict(l=0, r=0, b=0, t=30)
)

fig.show()
In [46]:
# Нормализуем размер точек от 5 до 20 пикселей
price_min = df_filtered['Маркетинговая цена'].min()
price_max = df_filtered['Маркетинговая цена'].max()
sizes = 5 + 15 * (df_filtered['Маркетинговая цена'] - price_min) / (price_max - price_min)

fig.update_traces(marker=dict(size=sizes))
In [47]:
# Если кластеры еще не определены
from sklearn.cluster import DBSCAN
clustering = DBSCAN(eps=3, min_samples=5).fit(X_tsne_filtered)
df_filtered['cluster'] = clustering.labels_
In [48]:
# Визуализация исходных выбросов
outliers = df_tsne[distances.flatten() >= threshold]
px.scatter_3d(outliers, x='tsne_x', y='tsne_y', z='tsne_z', 
              title='Выбросы, исключенные из анализа')

1. Цель t-SNE в анализе¶

t-SNE (t-Distributed Stochastic Neighbor Embedding) — это метод визуализации многомерных данных, который помогает выявить скрытые паттерны и кластеры товаров на основе их ценовых характеристик. В вашем случае:

  • Входные данные:

    • Маркетинговая цена
    • Соотношение цен (ваша цена / цена конкурентов)
    • Длина наименования (если есть)
  • Выход:

    • 3D-проекция, где близость точек указывает на схожесть товаров по ценовым параметрам.

2. Ключевые выводы из визуализации¶

a) Выявленные кластеры (группы товаров):¶

Кластер 0 (Средняя цена: 357 руб, соотношение: 0.90):¶

  • Характеристики:
    • Товары дешевле конкурентов (~10%)
  • Рекомендация:
    • Проверить маржинальность
    • Возможен потенциал для повышения цены до уровня рынка (0.95–1.0 от конкурентов)

Кластер 1 (Средняя цена: 749 руб, соотношение: 1.18):¶

  • Характеристики:
    • Товары дороже конкурентов (~18%), но с высокой абсолютной ценой
  • Рекомендация:
    • Усилить USP (уникальные преимущества)
    • Проверить, оправдана ли наценка (качество, бренд, дополнительные услуги)

Кластер 2 (Средняя цена: 346 руб, соотношение: 1.42):¶

  • Характеристики:
    • Товары значительно дороже конкурентов (42%)
  • Риск:
    • Потеря продаж из-за завышения цены
  • Действия:
    • Срочный пересмотр цен (постепенное снижение до 1.2–1.3 от конкурентов)
    • Акцентирование уникальности

b) Выбросы (outliers):¶

Точки, далеко отстоящие от основных кластеров, могут быть:

  1. Уникальные товары без аналогов (например, товары без данных о конкурентах)
  2. Ошибки в данных (некорректные цены)
  3. Стратегические позиции (например, товары-локомотивы для привлечения трафика)

3. Практические рекомендации¶

a) Для кластеров:¶

Товары дешевле рынка (Cluster 0):¶

  • Постепенно повышайте цены до уровня конкурентов, если маржа позволяет
  • Используйте A/B-тесты для проверки ценовой эластичности

Товары с умеренной наценкой (Cluster 1):¶

  • Поддерживайте текущий уровень цен
  • Усильте маркетинг (например, выделите преимущества в карточке товара)

Товары с завышенной ценой (Cluster 2):¶

  • Снижайте цены поэтапно (на 10–15% каждые 2 недели), отслеживая динамику продаж
  • Для уникальных товаров добавьте описание уникальных характеристик

In [ ]: